package edu.gmu.atelier;

import java.util.ArrayList;
import java.util.List;

/**
 * Set of vectors with vector combination and scalar multiplication operations.
 * @author James H. Pope
 */
public class Matrix implements IMatrix
{
    private List<Vector> vectors = null;
    
    private VectorFactory factory = null;
    
    public Matrix( Vector[] vs, VectorFactory factory )
    {
        this.vectors = new ArrayList<Vector>();
        for (int i = 0; i < vs.length; i++)
        {
            this.vectors.add(i, vs[i]);
        }
        this.factory = factory;
    }
    
    public Matrix( List<Vector> vectors, VectorFactory factory )
    {
        this.vectors = vectors;
        this.factory = factory;
    }
    
    public int getRows()
    {
        return this.vectors.get(0).getLength();
    }

    public int getCols()
    {
        return this.vectors.size();
    }
    
    public Element getValue( int row, int col )
    {
        // May be inefficient is new Element objects have to be created.
        return this.getVector(col).getElement(row);
    }
    
    public void setValue( int row, int col, Element e )
    {
        // May be inefficient is new Element objects have to be created.
        this.getVector(col).setElement(row, e);
    }
    
    public VectorFactory getFactory()
    {
        return this.factory;
        //return this.vectors.get(0).getFactory();
    }
    
    public Matrix copy()
    {
        List<Vector> u = new ArrayList<Vector>();
        for (int i = 0; i < this.getCols(); i++)
        {
            u.add( i, this.vectors.get(i).copy() );
        }
        return new Matrix(u, this.factory);
    }
    
    public Matrix copyMinor(int row, int col)
    {
        int n = this.getCols();
        int m = this.getRows();
        int cols = n - col;
        int len  = m - row;
        List<Vector> u = new ArrayList<Vector>();
        for (int c = 0; c < cols; c++)
        {
            Vector v = this.vectors.get(col+c);
            u.add( c, v.copy(row, len) );
        }
        return new Matrix(u, this.factory);
    }
    
    //------------------------------------------------------------------------//
    // Simplex Method utilities
    //------------------------------------------------------------------------//
    public Matrix extract(int[] B)
    {
        // Note that this gets rows at index set defined by B
        int n = this.getCols();
        if( B.length != n )
        {
            String s = "Incompatible sizes B.len="+B.length+" A.n="+n;
            throw new IllegalArgumentException(s);
        }
        
        List<Vector> vs = new ArrayList<Vector>();
        for (int i = 0; i < B.length; i++)
        {
            vs.add( this.factory.makeVector( n ) );
        }
        Matrix ab = new Matrix(vs, this.factory);
        extractTo(B, ab);
        return ab;
    }
    
    public void extractTo(int[] B, Matrix ab)
    {
        // I believe ab has to be square
        int n = ab.getCols();
        int m = ab.getRows();
        if( n != m )
        {
            String s = "Incompatible Ab n="+n+" m="+m;
            throw new IllegalArgumentException(s);
        }
        
        for (int i = 0; i < ab.getCols(); i++)
        {
            Vector bv = ab.getVector(i);
            Vector av = this.getVector(i);
            // delegate, specific implementations may be faster
            av.extractTo(B, bv);
        }
    }
    //------------------------------------------------------------------------//
    
    public Matrix copy(int start, int length)
    {
        int n = this.getCols();
        int m = this.getRows();
        List<Vector> u = new ArrayList<Vector>();
        for (int i = 0; i < length; i++)
        {
            Vector v = this.vectors.get(start+i);
            u.add( i, v.copy(start, length) );
        }
        return new Matrix(u, this.factory);
    }
    
    public Matrix copyElements(int startRow, int startCol, Matrix subm)
    {
        List<Vector> u = new ArrayList<Vector>();
        for (int i = 0; i < this.getCols(); i++)
        {
            u.add( this.getVector(i).copy() );
        }
        
        int n = subm.getCols();
        for (int c = 0; c < n; c++)
        {
            // Expected that v.len >= startRow + s.len
            Vector v = u.get(startCol+c);
            Vector s = subm.getVector(c);
            v.copyElements(startRow, s);
        }
        return new Matrix(u, this.factory);
    }
    
    public Matrix identity()
    {
        return this.factory.makeIdentity(this.getCols());
    }
    
    // Potentially expensive call
    public void swapRow( int fromRow, int toRow )
    {
        int n = this.getCols();
        for (int j = 0; j < n; j++)
        {
            Vector v = vectors.get(j);
            v.swap(fromRow, toRow);
        }
    }
    
    // Very efficient - just swaps pointers
    public void swapCol( int fromCol, int toCol )
    {
        Vector v = this.vectors.get(fromCol);
        this.vectors.set(fromCol, this.vectors.get(toCol));
        this.vectors.set(toCol,   v);
    }
    
    public void mulAddTo( int fromRow, Element mult, int toRow )
    {
        int n = this.getCols();
        for (int j = 0; j < n; j++)
        {
            Vector v = vectors.get(j);
            v.mulAddTo(fromRow, mult, toRow);
        }
    }
    
    public void mulTo( int row, Element mult )
    {
        int n = this.getCols();
        for (int j = 0; j < n; j++)
        {
            Vector v = vectors.get(j);
            v.mulTo(row, mult);
        }
    }
    
    public Matrix add(IMatrix m)
    {
        List<Vector> a_b = new ArrayList<Vector>();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            Vector b = m.getVector(i);
            a_b.add( a.add(b) );
        }
        return new Matrix( a_b, this.factory );
    }
    
    public Matrix sub(IMatrix m)
    {
        List<Vector> a_b = new ArrayList<Vector>();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            Vector b = m.getVector(i);
            a_b.add( a.sub(b) );
        }
        return new Matrix( a_b, this.factory );
    }
    
    public Matrix mult(Element s)
    {
        List<Vector> ea = new ArrayList<Vector>();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            ea.add( a.mult(s) );
        }
        return new Matrix( ea, this.factory );
    }
    
    public Matrix diag()
    {
        List<Vector> vs = new ArrayList<Vector>();
        int m = this.getRows();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            Vector v = this.factory.makeVector(m);
            v.setElement(i, a.getElement(i));
            vs.add( v );
        }
        return new Matrix( vs, this.factory );
    }
    
    public Matrix sqrt()
    {
        // Can only take sqrt of a diagonal matrix
        if( this.isDiagonal() == false )
        {
            String s = "Cannot take sqrt of a non-diaginal matrix.";
            throw new IllegalStateException(s);
        }
        List<Vector> vs = new ArrayList<Vector>();
        int m = this.getRows();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            Vector v = this.factory.makeVector(m);
            v.setElement(i, a.getElement(i).sqrt() );
            vs.add( v );
        }
        return new Matrix( vs, this.factory );
    }
    
    public Matrix inverse()
    {
        // Can only easily take inverse of a diagonal matrix, use decomposition
        if( this.isDiagonal() == false )
        {
            String s = "Cannot take inverse of a non-diaginal matrix, use LU.";
            throw new IllegalStateException(s);
        }
        List<Vector> vs = new ArrayList<Vector>();
        int m = this.getRows();
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector a = this.vectors.get(i);
            Vector v = this.factory.makeVector(m);
            v.setElement(i, a.getElement(i).inverse() );
            vs.add( v );
        }
        return new Matrix( vs, this.factory );
    }
    
    public boolean isDiagonal()
    {
        for (int i = 0; i < vectors.size(); i++)
        {
            Vector v = vectors.get(i);
            for (int j = 0; j < v.getLength(); j++)
            {
                Element e = v.getElement(j);
                if( i != j && e.isZero() == false )
                {
                    return false;
                }
            }
        }
        return true;
    }
    
    public boolean isOrthogonal()
    {
        for (int i = 0; i < this.getCols(); i++)
        {
            Vector v = this.getVector(i);
            for (int j = 0; j < this.getCols(); j++)
            {
                Vector u = this.getVector(j);
                if( v == u )
                {
                    continue;
                }
                // May have to add epsilon check here
                if( v.dot(u).isZero() == false)
                {
                    return false;
                }
            }
        }
        return true;
    }
    
    public boolean isIdentity( )
    {
        Element one  = vectors.get(0).getElement(0).getOne();
        Element zero = vectors.get(0).getElement(0).getZero();
        for (int i = 0; i < vectors.size(); i++)
        {
            Vector vector = vectors.get(i);
            //Element diag = vector.getElement(i);
            for (int j = 0; j < vector.getLength(); j++)
            {
                Element elem = vector.getElement(j);
                if( i == j )
                {
                    if( elem.equals( one  ) == false ) return false;
                }
                else
                {
                    if( elem.equals( zero ) == false ) return false;
                }
            }
        }
        return true;
    }
    
    public boolean isIdentity( Epsilon e )
    {
        Element one  = vectors.get(0).getElement(0).getOne();
        Element zero = vectors.get(0).getElement(0).getZero();
        for (int i = 0; i < vectors.size(); i++)
        {
            Vector vector = vectors.get(i);
            //Element diag = vector.getElement(i);
            for (int j = 0; j < vector.getLength(); j++)
            {
                Element elem = vector.getElement(j);
                if( i == j )
                {
                    if( elem.similar( one , e) == false ) return false;
                }
                else
                {
                    if( elem.similar( zero ,e) == false ) return false;
                }
            }
        }
        return true;
    }
    
    public boolean isDiagIdentity( Epsilon e )
    {
        Element one  = vectors.get(0).getElement(0).getOne();
        for (int i = 0; i < vectors.size(); i++)
        {
            Vector vector = vectors.get(i);
            Element diag = vector.getElement(i);
            if( diag.similar( one , e) == false )
            {
                return false;
            }
        }
        return true;
    }
    
    //Testing only
    public double[][] getData()
    {
        double[][] data = new double[this.getRows()][this.getCols()];
        for (int i = 0; i < data.length; i++)
        {
            for (int j = 0; j < data.length; j++)
            {
                data[i][j] = this.vectors.get(j).getElement(i).doubleValue();
            }
        }
        return data;
    }
    
    public Matrix normalize()
    {
        List<Vector> vs = new ArrayList<Vector>();
        for (int i = 0; i < vectors.size(); i++)
        {
            Vector v = vectors.get(i);
            Vector u = v.normalize();
            vs.add(i,u);
        }
        return new Matrix(vs, this.getFactory());
    }
    
    public List<Vector> getVectors()
    {
        return this.vectors;
    }

    /**
     * Right multiplies this matrix by the given vector or
     * left multiplies the given vector by this matrix.  Said differently
     * linearly transforms (maps) vector x to returned vector b using
     * f: R^N -> R^M
     * @param x
     * @return b = this*x
     */
    public Vector mult(Vector x)
    {
        return x.mult(this);
    }

    /**
     * Right multiplies this matrix by the specified matrix.
     * @param b
     * @return this*b
     */
    public Matrix mult( IMatrix b )
    {
        List<Vector> c = new ArrayList<Vector>();
        for (int j = 0; j < b.getCols(); j++)
        {
            Vector bi = b.getVector(j);
            c.add( this.mult(bi) );
        }
        return new Matrix(c, this.factory);
    }

    public Vector getVector(int j)
    {
        return this.vectors.get(j);
    }

    public Matrix transpose()
    {
        return this.vectors.get(0).transpose(this);
    }
    
//    public boolean isEigenVector( Vector x )
//    {
//        Vector b = x.dot(this);
//        // Find first element that is not zero
//        int m = b.getLength();
//        Element element = null;
//        for (int i = 0; i < m; i++)
//        {
//            Element e = b.getElement(0);
//            if(e.isZero() == false)
//            {
//                element = e;
//                break;
//            }
//        }
//        
//    }

    /**
     * Produces a new square matrix of size m x m (m = x.len = y.len)
     * with the columns of y[0]*x, y[1]x, ..., y[m]x = x*y^T.
     * This right multiplies x by the transform of y.
     * @param x
     * @param y
     * @return x*y^T
     */
    public Matrix mult( Vector x, Vector y ) // left multiply by A, i.e.  b = Ax
    {
        // sanity check
        if( x.getLength() != y.getLength() )
        {
            String s = "Incompatible vector lengths for multiplication, x="+
                    x.getLength()+" y="+y.getLength();
            throw new IllegalArgumentException(s);
        }
        
        int m = x.getLength();
        List<Vector> xyt = new ArrayList<Vector>();
        for (int i = 0; i < m; i++)
        {
            Element yi = y.getElement(i);
            Vector xi = x.copy();
            for (int j = 0; j < m; j++)
            {
                xi.mulTo(j, yi);
            }
            xyt.add(xi);
        }
        /*
         * Vector could have done all this except for creating the matrix itself
         */
        return new Matrix(xyt, this.factory);
    }
    
    public boolean similar( Object obj, Epsilon e )
    {
        if(obj == this)
        {
            return true;
        }
        
        if( (obj instanceof Matrix) == false )
        {
            return false;
        }
        
        Matrix o = (Matrix)obj;
        
        if( o.vectors.size() != this.vectors.size() )
        {
            return false;
        }
        
        for (int i = 0; i < this.vectors.size(); i++)
        {
            if( this.vectors.get(i).similar(o.vectors.get(i), e) == false )
            {
                return false;
            }
        }
        
        return true;
    }
    
    
    //------------------------------------------------------------------------//
    // Class instance methods
    //------------------------------------------------------------------------//
    public boolean equals( Object obj )
    {
        return this.similar(obj, null);
    }
    
    public String toString()
    {
        int n = this.getCols();
        int m = this.getRows();
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < m; i++)
        {
            buf.append( "\n[");
            for (int j = 0; j < n; j++)
            {
                buf.append(vectors.get(j).getElement(i));
                buf.append( "," );
            }
            buf.append( "]");
        }
        return buf.toString();
    }

}
